use std::{path::PathBuf, sync::Arc};

use sea_orm::DatabaseConnection;
use thiserror::Error;
use tokio::sync::{oneshot, RwLock};
use tracing::*;

use aspect_client::{
    device_resolver,
    errors::Error,
    peer_manager::{self, PeerAuthConfig},
    signer_manager,
};
use aspect_client_rpc::ClientRpcServer;
use aspect_db_common::prelude::*;
use aspect_svc_common::{logging, make_rt};

mod args;
mod config;
mod rpc_server;

use config::Config;

fn main() {
    logging::init();
    info!("starting...");

    let args = argh::from_env::<args::Args>();

    let config = Config {
        datadir: args.datadir,
        db_url: args.database_url.expect("init: missing database url"),
        rpc_port: args.rpc_port,
    };

    let rt = make_rt("aspect-user");

    if let Err(e) = rt.block_on(main_task(&config)) {
        error!(err = %e, "STOPPING");
    }

    info!("exiting...");
}

// TODO switch to using the chained error thing?
#[derive(Debug, Error)]
pub enum InitError {
    #[error("ident: {0}")]
    Ident(#[from] aspect_ident::errors::Error),

    #[error("client: {0}")]
    Client(#[from] aspect_client::errors::Error),

    #[error("peering: {0}")]
    PeerManager(#[from] aspect_client::peer_manager::Error),

    #[error("datastore: {0}")]
    Datastore(#[from] aspect_db_common::errors::Error),
}

async fn main_task(config: &Config) -> Result<(), InitError> {
    // Prep the database.
    let dbc = sea_orm::Database::connect(&config.db_url)
        .await
        .expect("init: db connect");

    let n_applied = apply_migrations(&dbc)
        .await
        .expect("init: apply migrations");
    if n_applied > 0 {
        info!(migrations = %n_applied, "connected, applied migrations");
    } else {
        info!("connected, no migrations to apply");
    }

    // Ready the device signer.
    let sm_inner = open_or_create_device(&config.datadir, config).await?;
    let internal_signer = Arc::new(sm_inner.get_internal_signer());
    let ident = *sm_inner.current_update_pkg().ident();
    info!(%ident, "loaded device");
    let signer_man = Arc::new(RwLock::new(sm_inner));

    // Set up the datastore wrappers.
    let state_prop_ds = Arc::new(StatePropDatastore::new(dbc.clone())?);
    let ident_ds = Arc::new(DeviceDatastore::new(dbc)?);

    let peer_auth_config = PeerAuthConfig::Ident(internal_signer);
    let peer_man = Arc::new(
        peer_manager::PeerManager::new(state_prop_ds, signer_man.clone(), peer_auth_config).await?,
    );
    let dev_resolv = Arc::new(device_resolver::DeviceResolver::new(
        peer_man.clone(),
        ident_ds.clone(),
    ));

    // If we're registered to a homeserver, reannounce our device to them.
    try_reannounce_ident(&peer_man).await?;

    // Now that everything is set up we can start the RPC listener.
    let rpc_listen_addr = format!("127.0.0.1:{}", config.rpc_port);
    let (stop_tx, stop_rx) = oneshot::channel();
    let rpc_impl = rpc_server::RpcImpl::new(peer_man, dev_resolv, signer_man, stop_tx);
    let rpc_server = jsonrpsee::server::ServerBuilder::new()
        .build(&rpc_listen_addr)
        .await
        .expect("init: build rpc server");
    let rpc_han = rpc_server.start(rpc_impl.into_rpc());

    info!(%rpc_listen_addr, "started!");

    // Wait to be told to exit.
    tokio::select! {
        res = tokio::signal::ctrl_c() => {
            res.expect("main: wait for C-c");
        }
        _ = stop_rx => {
            // nothing?
        }
    }

    info!("received stop signal, beginning shutdown...");
    let _ = rpc_han.stop();

    Ok(())
}

async fn apply_migrations(dbc: &DatabaseConnection) -> Result<usize, sea_orm::DbErr> {
    use aspect_db_sql_migration::{Migrator, MigratorTrait};

    let pending = Migrator::get_pending_migrations(dbc).await?;
    if !pending.is_empty() {
        Migrator::up(dbc, None).await?;
    }

    Ok(pending.len())
}

/// Given a datadir path, inits the signer manager by opening or creating a new device file.
async fn open_or_create_device(
    datadir: &PathBuf,
    config: &Config,
) -> Result<signer_manager::SignerManager, InitError> {
    let mut dev_path = datadir.clone();
    dev_path.push("device.json");
    if dev_path.exists() {
        Ok(signer_manager::SignerManager::open(dev_path).await?)
    } else {
        let sb = aspect_ident::SignerBuilder::new("testuser".to_owned());
        let (signer, ident) = sb.finalize()?;
        Ok(signer_manager::SignerManager::create(dev_path, signer).await?)
    }
}

async fn try_reannounce_ident(pm: &Arc<peer_manager::PeerManager>) -> Result<(), Error> {
    // TODO
    Ok(())
}
